package com.wsoft.oss.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.wsoft.constant.RedisKeyConstant;
import com.wsoft.core.exception.Asserts;
import com.wsoft.core.service.RedisService;
import com.wsoft.oss.entity.OssFileEntity;
import com.wsoft.oss.enums.BucketNameEnum;
import com.wsoft.oss.enums.FileSizeEnum;
import com.wsoft.oss.enums.FileUpdateStatusEnum;
import com.wsoft.oss.manager.OssFileManager;
import com.wsoft.oss.query.MergePartQuery;
import com.wsoft.oss.query.PartUploadQuery;
import com.wsoft.oss.query.SecondUploadQuery;
import com.wsoft.oss.service.IOssService;
import com.wsoft.oss.utils.MinioTemplate;
import com.wsoft.oss.utils.OssUtil;
import com.wsoft.oss.vo.FileUploadVO;
import com.wsoft.utils.FileUtil;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author z001
 * @Date 2024/9/26 10:39
 */
public class OssMinioServiceImpl implements IOssService {

    @Value("${file.type}")
    private String storageType;
    @Value("${file.minio.breakpointTime}")
    private long breakpointTime;
    @Value("${file.max-size}")
    private Integer maxSize;
    @Resource
    private MinioTemplate minioTemplate;
    @Resource
    private OssFileManager ossFileManager;
    @Resource
    private RedisService redisService;

    /**
     * 5MB
     */
    private static final Integer BUFFER_SIZE = 1024 * 1024 * 5;
    /**
     * 上传分片
     *
     * @return
     */
    @Override
    public void uploadPart(PartUploadQuery uploadQuery) throws Exception {
        Boolean b = redisService.sIsMember(RedisKeyConstant.Oss.OSS_PART_INDEX + uploadQuery.getMd5(), uploadQuery.getUploadPartCount());
        if (b) {
            //如果已经上传过 则不继续上传
            return;
        }
        Object configCache = redisService.get(RedisKeyConstant.CONFIG_CACHE_ALL);
        int fileMaxSize =  OssUtil.getFileMaxSize(configCache,maxSize);

        Object object = redisService.get(RedisKeyConstant.Oss.OSS_FILE_SIZE + uploadQuery.getMd5());
        Long fileSizeObj = object == null? 0L :  Long.valueOf(object.toString());
        long sizeKb = uploadQuery.getFilePart().getSize() / (FileUtil.MB);
        if((fileSizeObj + sizeKb)/FileUtil.MB > fileMaxSize){
            //文件超出最大限制
            Asserts.fail("[oss.file.maximum.limit]");
        }

        // 构建每个part的object name
        MultipartFile filePart = uploadQuery.getFilePart();
        //临时地址
        String temporaryPath = OssUtil.buildMinioDirTemporaryPath(uploadQuery.getMd5());
        String partObjectName = uploadQuery.getFileName() + OssUtil.PART + uploadQuery.getUploadPartCount();
        InputStream partStream = filePart.getInputStream();
        String bucketName = BucketNameEnum.resolve(uploadQuery.getBucketType()).getBucketName();
        minioTemplate.uploadFile(bucketName, temporaryPath + partObjectName, partStream);

        //赋值redis 上传分片的数量
        redisService.sAdd(RedisKeyConstant.Oss.OSS_PART_INDEX + uploadQuery.getMd5(), uploadQuery.getUploadPartCount());
        redisService.expire(RedisKeyConstant.Oss.OSS_PART_INDEX + uploadQuery.getMd5(), breakpointTime);
        redisService.sAdd(RedisKeyConstant.Oss.OSS_FILE_NAME + uploadQuery.getMd5(), partObjectName);
        redisService.expire(RedisKeyConstant.Oss.OSS_FILE_NAME + uploadQuery.getMd5(), breakpointTime);

        //设置part文件 生命周期,1天
        minioTemplate.setBucketLifecycle(convertSecondsToDays(breakpointTime), OssUtil.TEMPORARY_URL + uploadQuery.getMd5(), uploadQuery.getMd5(), bucketName);
        redisService.incr(RedisKeyConstant.Oss.OSS_FILE_SIZE + uploadQuery.getMd5(),sizeKb );
        redisService.expire(RedisKeyConstant.Oss.OSS_FILE_SIZE + uploadQuery.getMd5(), RedisKeyConstant.Oss.EXPIRED);
    }

    /**
     * 秒传
     */
    @Override
    public FileUploadVO secondUpload(SecondUploadQuery query) throws Exception {
        FileUploadVO vo = new FileUploadVO();
        OssFileEntity ossFile = ossFileManager.getMd5(query.getMd5());
        if (ossFile != null) {
            String newUuid = OssUtil.getUuid();
            String newName = OssUtil.changeNewFileName(ossFile.getName(), newUuid);
            String bucketName = BucketNameEnum.resolve(query.getBucketType()).getBucketName();
            //拷贝文件
            minioTemplate.copyFile(ossFile.getBucketName(), ossFile.getUrl(), bucketName,OssUtil.buildMinioDirPath() +  newName);
            //赋值 新属性
            OssFileEntity newFile = new OssFileEntity();
            newFile.setFileType(ossFile.getFileType());
            newFile.setFileSuffix(ossFile.getFileSuffix());
            newFile.setStorageType(ossFile.getStorageType());
            newFile.setName(query.getFileName());
            newFile.setSize(ossFile.getSize());
            newFile.setUuid(Long.valueOf(newUuid));
            newFile.setUrl(OssUtil.buildMinioDirPath() + newName);
            newFile.setBucketName(bucketName);
            //秒传文件 这边不赋值 md5
            ossFileManager.save(newFile);

            //判断存在 则直接显示上传成功,并返回id
            vo.setFileUpdateStatus(FileUpdateStatusEnum.UPLOADED.getType());
            vo.setId(newFile.getId());
            vo.setFileName(newFile.getName());
        } else {
            //文件不存在,不能秒传
            Asserts.fail("[oss.file.not.exist.not.second.upload]");
        }
        return vo;
    }

    /**
     * 合并分片 文件
     *
     * @param uploadQuery
     * @throws Exception
     */
    @Override
    public FileUploadVO mergePart(MergePartQuery uploadQuery) throws Exception {
        String md5 = uploadQuery.getMd5();
        String bucketName = BucketNameEnum.resolve(uploadQuery.getBucketType()).getBucketName();
        Set<Object> partIndexSet = redisService.sMembers(RedisKeyConstant.Oss.OSS_PART_INDEX + md5);
        Set<Object> partNameSet = redisService.sMembers(RedisKeyConstant.Oss.OSS_FILE_NAME + md5);
        if(CollUtil.isEmpty(partIndexSet) || CollUtil.isEmpty(partNameSet)){
            Asserts.fail("[oss.file.part.not.exist]");
        }

        //必须按照part排序,minio 只有最后一块分片可以小于5mb,其他都必须大于5mb
        partNameSet = partSort(partNameSet);
        //获取需要合并的分片组装成ComposeSource
        List<ComposeSource> sourceObjectList = new ArrayList<>(partIndexSet.size());
        for (Object chunk : partNameSet) {
            sourceObjectList.add(ComposeSource.builder()
                    .bucket(bucketName)
                    .object(OssUtil.buildMinioDirTemporaryPath(md5) + chunk.toString())
                    .build());
        }
        //文件名称 原名称
        String newFileName = OssUtil.getPartName(partNameSet);
        String uuid = OssUtil.getUuid();
        String fileType = FileUtil.getExtensionName(newFileName);
        //名称 带路径 uuid.fileType
        String objectName = OssUtil.buildMinioObjectName(uuid, fileType);

        //合并分片
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket(bucketName)
                //合并后的文件的objectName
                .object(objectName)
                //指定源文件
                .sources(sourceObjectList)
                .build();
        minioTemplate.composeObject(composeObjectArgs);

        //删除已经上传的分片，组装成DeleteObject
        List<DeleteObject> collect = partNameSet.stream().map(item -> {
            return new DeleteObject(item.toString());
        }).collect(Collectors.toList());
        //执行删除
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(bucketName)
                .objects(collect)
                .build();
        Iterable<Result<DeleteError>> results = minioTemplate.removeObjects(removeObjectsArgs);
        try {
            for (Result<DeleteError> result : results) {
                DeleteError deleteError = result.get();
            }
        } catch (Exception e) {
            System.out.println("minio删除文件失败");
            e.printStackTrace();
        }
        Object fileKbSize = redisService.get(RedisKeyConstant.Oss.OSS_FILE_SIZE + md5);

        //删除缓存
        redisService.del(RedisKeyConstant.Oss.OSS_PART_INDEX + md5);
        redisService.del(RedisKeyConstant.Oss.OSS_FILE_NAME + md5);
        redisService.del(RedisKeyConstant.Oss.OSS_FILE_SIZE + md5);

        //新增file表
        OssFileEntity ossFileEntity = new OssFileEntity();
        ossFileEntity.setUuid(Long.valueOf(uuid));
        ossFileEntity.setFileType(FileUtil.getFileType(fileType));
        ossFileEntity.setFileSuffix(fileType);
        ossFileEntity.setName(newFileName);
        ossFileEntity.setUrl(objectName);
        ossFileEntity.setBucketName(bucketName);
        ossFileEntity.setStorageType(storageType);
        ossFileEntity.setMd5(md5);
        ossFileEntity.setSize(new BigDecimal(fileKbSize.toString()));
        ossFileManager.save(ossFileEntity);

        //返回值
        FileUploadVO vo = new FileUploadVO();
        vo.setId(ossFileEntity.getId());
        vo.setFileName(newFileName);
        return vo;
    }

    /**
     * 分片下载
     * @param id
     * @param picType
     * @param request
     * @param response
     */
    @Override
    public byte[] downloadPart(OssFileEntity fileEntity, Integer picType, String range, HttpServletResponse response) throws Exception {
        if(null == fileEntity){
            Asserts.fail("[oss.file.not.exist]");
        }
        //获取文件url
        String url = OssUtil.getFileUrl(fileEntity, picType);
        StatObjectResponse objectResponse = minioTemplate.statObject(fileEntity.getBucketName(),url);
        //计算起始和结束位置
        Map<String, Long> sizeMap = OssUtil.buildSize(range, objectResponse.size());
        long startByte = sizeMap.get("startByte");
        long endByte = sizeMap.get("endByte");

        // buffer 写入流
        BufferedOutputStream os = null;
        // minio 文件流
        GetObjectResponse stream = null;

        // 要下载的长度
        // 确保返回的 contentLength 不会超过文件的实际剩余大小
        long contentLength = Math.min(endByte - startByte, objectResponse.size() - startByte);
        if(contentLength <= 0){
            return new byte[]{};
        }
        try {
            // 获取文件流
            stream = minioTemplate.getObject(fileEntity.getBucketName(),objectResponse.object(), startByte, contentLength);
            os = new BufferedOutputStream(response.getOutputStream());
            // 将读取的文件写入到 OutputStream
            byte[] bytes = new byte[BUFFER_SIZE];
            long bytesWritten = 0;
            int bytesRead = -1;
            while ((bytesRead = stream.read(bytes)) != -1) {
                if (bytesWritten + bytesRead >= contentLength) {
                    os.write(bytes, 0, (int)(contentLength - bytesWritten));
                    break;
                } else {
                    os.write(bytes, 0, bytesRead);
                    bytesWritten += bytesRead;
                }
            }
            os.flush();
            response.flushBuffer();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                os.close();
            }
            if (stream != null) {
                stream.close();
            }
        }
        return null;
    }

    @Override
    public boolean deleteFile(Long fileId) throws Exception {
        OssFileEntity fileEntity = ossFileManager.getById(fileId);
        if(ObjUtil.isEmpty(fileEntity)){
            return true;
        }
        boolean b = ossFileManager.removeById(fileId);
        if(b){
            minioTemplate.removeFile(fileEntity.getBucketName(),fileEntity.getUrl());
        }
        return b;
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<FileUploadVO> uploadList(MultipartFile[] multipartFileList, Integer bucketType) throws Exception {
        Object configCache = redisService.get(RedisKeyConstant.CONFIG_CACHE_ALL);
        int fileMaxSize =  OssUtil.getFileMaxSize(configCache,maxSize);
        //校验文件大小
        OssUtil.checkFileSize(multipartFileList,fileMaxSize);

        List<FileUploadVO> voList = new ArrayList<>();
        List<OssFileEntity> fileList = new ArrayList<>();
        for (MultipartFile multipartFile : multipartFileList) {
            OssFileEntity fileEntity = this.upload(multipartFile, bucketType);
            fileList.add(fileEntity);
        }
        ossFileManager.saveBatch(fileList);
        for (OssFileEntity ossFileEntity : fileList) {
            FileUploadVO vo = new FileUploadVO();
            vo.setId(ossFileEntity.getId());
            vo.setFileName(ossFileEntity.getName());
            voList.add(vo);
        }
        return voList;
    }

    /**
     * 返回文件 InputStreamResource
     *
     * @param ossFile
     * @return
     */
    @Override
    public InputStreamResource getInputStream(OssFileEntity ossFile, Integer picType) {
        try {
            //判断文件是否存在
            boolean exist = minioTemplate.isObjectExist(ossFile.getBucketName(), ossFile.getUrl());
            if (!exist) {
                Asserts.fail("[oss.file.not.exist]");
            }
            InputStream object = minioTemplate.getObject(ossFile.getBucketName(), ossFile.getUrl());
            //是图片 且 picType 不是原图的时候 判断并重新生成图片
            if (ossFile.getFileType().equals(FileUtil.IMAGE)
                    && FileSizeEnum.resolve(picType) != null) {
                FileSizeEnum sizeEnum = FileSizeEnum.resolve(picType);
                return this.changPicSize(ossFile, sizeEnum, object);
            }
            return new InputStreamResource(object);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    // ################################## 私有方法  ##################################

    /**
     * 上传minio
     *
     * @param multipartFile
     * @param bucketType
     * @return
     * @throws Exception
     */
    public OssFileEntity upload(MultipartFile multipartFile, Integer bucketType) throws Exception {
        String bucketName = BucketNameEnum.resolve(bucketType).getBucketName();
        String uuid = OssUtil.getUuid();
        String objectName = OssUtil.buildMinioDirPath() + OssUtil.changeNewFileName(multipartFile.getOriginalFilename(), uuid);

        //上传文件
        minioTemplate.uploadFile(bucketName, multipartFile, objectName, multipartFile.getContentType());

        String fileType = FileUtil.getExtensionName(multipartFile.getOriginalFilename());
        OssFileEntity ossFileEntity = new OssFileEntity();
        ossFileEntity.setUuid(Long.valueOf(uuid));
        ossFileEntity.setFileType(FileUtil.getFileType(fileType));
        ossFileEntity.setFileSuffix(fileType);
        ossFileEntity.setName(multipartFile.getOriginalFilename());
        ossFileEntity.setSize(FileUtil.getKbSize(multipartFile.getSize()));
        ossFileEntity.setUrl(objectName);
        ossFileEntity.setBucketName(bucketName);
        ossFileEntity.setStorageType(storageType);
        return ossFileEntity;
    }


    /**
     * 获取inputStream流后 缩放图片 并返回 InputStream
     *
     * @param inputStream inputStream流
     * @param fileSuffix  图片后缀名 不带.
     * @param width       缩放后宽度
     * @param height      缩放后高度
     * @return
     * @throws IOException
     */
    public static InputStream resizeImage(InputStream inputStream, String fileSuffix, int width, int height) throws IOException {
        // 读取InputStream到BufferedImage
        BufferedImage originalImage = ImageIO.read(inputStream);

        // 使用Thumbnails进行缩放
        BufferedImage resizedImage = Thumbnails.of(originalImage)
                .size(width, height)
                .asBufferedImage();

        // 将缩放后的BufferedImage写入到ByteArrayOutputStream
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(resizedImage, fileSuffix, baos);

        // 将ByteArrayOutputStream转换为InputStream
        return new ByteArrayInputStream(baos.toByteArray());
    }


    /**
     * 修改图片 大小 并上传
     *
     * @return
     * @throws Exception
     */
    private InputStreamResource changPicSize(OssFileEntity ossFile, FileSizeEnum sizeEnum, InputStream object) throws Exception {
        //构建 修改后的 图片名称
        String fileName = OssUtil.buildChangePicName(String.valueOf(ossFile.getUuid()), sizeEnum.getType(), ossFile.getFileSuffix());
        //url
        String objectName = OssUtil.buildMinioChangePicUrl(fileName);

        //获取min图
        if (Objects.equals(sizeEnum.getType(), FileSizeEnum.MIN.getType())) {
            if (StrUtil.isBlank(ossFile.getMinUrl())) {
                object = resizeImage(object, ossFile.getFileSuffix(), sizeEnum.getWidth(), sizeEnum.getHeight());
                //上传图片
                minioTemplate.uploadFile(ossFile.getBucketName(), objectName, object);
                //更新数据库
                ossFile.setMinUrl(objectName);
                ossFileManager.updateById(ossFile);
                return new InputStreamResource(object);
            } else {
                return new InputStreamResource(minioTemplate.getObject(ossFile.getBucketName(), ossFile.getMinUrl()));
            }
        } else if (Objects.equals(sizeEnum.getType(), FileSizeEnum.MID.getType())) {
            if (StrUtil.isBlank(ossFile.getMidUrl())) {
                object = resizeImage(object, ossFile.getFileSuffix(), sizeEnum.getWidth(), sizeEnum.getHeight());
                //上传图片
                minioTemplate.uploadFile(ossFile.getBucketName(), objectName, object);
                //更新数据库
                ossFile.setMidUrl(objectName);
                ossFileManager.updateById(ossFile);
                return new InputStreamResource(object);
            } else {
                return new InputStreamResource(minioTemplate.getObject(ossFile.getBucketName(), ossFile.getMidUrl()));
            }
        } else {
            if (StrUtil.isBlank(ossFile.getMaxUrl())) {
                object = resizeImage(object, ossFile.getFileSuffix(), sizeEnum.getWidth(), sizeEnum.getHeight());
                //上传图片
                minioTemplate.uploadFile(ossFile.getBucketName(), objectName, object);
                //更新数据库
                ossFile.setMaxUrl(objectName);
                ossFileManager.updateById(ossFile);
                return new InputStreamResource(object);
            } else {
                return new InputStreamResource(minioTemplate.getObject(ossFile.getBucketName(), ossFile.getMaxUrl()));
            }
        }

    }

    /**
     * 秒数转天数
     * @param seconds
     * @return
     */
    public static int convertSecondsToDays(long seconds) {
        if(seconds < RedisKeyConstant.Oss.EXPIRED){
            return 1;
        }
        //将秒数除以86400得到天数
        //返回天数
        return (int) (seconds / RedisKeyConstant.Oss.EXPIRED);
    }

    /**
     * 分片排序, minio分片上传 只有最后一个分片可以小于5mb
     * @param partNameSet
     * @return
     */
    private Set<Object> partSort(Set<Object> partNameSet){
        return partNameSet.stream().sorted(Comparator.comparingInt(item -> {
            Pattern pattern = Pattern.compile(OssUtil.PART_PATTERN);
            Matcher matcher = pattern.matcher(item.toString());
            matcher.find();
            return Integer.parseInt(matcher.group(1));
        })).collect(Collectors.toCollection(LinkedHashSet::new));
    }

    public static void main(String[] args) {
        Set<String> partNameSet = new HashSet<>();
        partNameSet.add("blob.part1");
        partNameSet.add("blob.part0");
        partNameSet.add("blob.part3");
        partNameSet.add("blob.part6");
        partNameSet.add("blob.part2");

        Set<String> collect = partNameSet.stream().sorted(Comparator.comparingInt(item -> {
            Pattern pattern = Pattern.compile(OssUtil.PART_PATTERN);
            Matcher matcher = pattern.matcher(item.toString());
            matcher.find();
            return Integer.parseInt(matcher.group(1));
        })).collect(Collectors.toCollection(LinkedHashSet::new));
        System.out.println(collect);
    }

}
